package org.jwos.platform.ajax;

import java.beans.PropertyDescriptor;
import java.io.StringReader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;

import com.sdicons.json.model.JSONArray;
import com.sdicons.json.model.JSONBoolean;
import com.sdicons.json.model.JSONDecimal;
import com.sdicons.json.model.JSONInteger;
import com.sdicons.json.model.JSONObject;
import com.sdicons.json.model.JSONString;
import com.sdicons.json.model.JSONValue;
import com.sdicons.json.parser.JSONParser;

public class JSONCoder {

	public static String encode(Object obj) throws Exception {
		return toJSONValue(obj);
	}

	public static Object decode(String str) throws Exception {
		JSONParser parser = new JSONParser(new StringReader(str));
		return toJavaValue(parser.nextValue());
	}

	protected static Object toJavaValue(JSONValue jvalue) throws Exception {
		Object value = null;

		if (jvalue.isBoolean()) {
			value = toJavaBoolean(jvalue);
		} else if (jvalue.isDecimal()) {
			if (toJavaClass(jvalue) == double.class) {
				value = toJavaDouble(jvalue);
			} else {
				value = toJavaFloat(jvalue);
			}
		} else if (jvalue.isInteger()) {
			if (toJavaClass(jvalue) == long.class) {
				value = toJavaLong(jvalue);
			} else if (toJavaClass(jvalue) == int.class) {
				value = toJavaInteger(jvalue);
			} else if (toJavaClass(jvalue) == short.class) {
				value = toJavaShort(jvalue);
			} else {
				value = toJavaByte(jvalue);
			}
		} else if (jvalue.isString()) {
			value = toJavaString(jvalue);
		} else if (jvalue.isObject()) {
			if (toJavaClass(jvalue) == Date.class) {
				value = toJavaDate(jvalue);
			} else if (toJavaClass(jvalue) == Map.class) {
				value = toJavaMap((JSONObject)jvalue);
			} else {
				value = toJavaObject((JSONObject) jvalue);
			}
		} else if (jvalue.isArray()) {
			value = toJavaList((JSONArray) jvalue);
		} else if (jvalue.isNull()) {
			value = null;
		}

		return value;
	}

	protected static boolean toJavaBoolean(JSONValue jvalue) {
		return ((JSONBoolean) jvalue).getValue();
	}

	protected static double toJavaDouble(JSONValue jvalue) {
		if (jvalue.isInteger()) {
			return new Double(toJavaLong(jvalue)).doubleValue();
		} else {
			return ((JSONDecimal) jvalue).getValue().doubleValue();
		}
	}

	protected static float toJavaFloat(JSONValue jvalue) {
		if (jvalue.isInteger()) {
			return new Float(toJavaLong(jvalue)).floatValue();
		} else {
			return ((JSONDecimal) jvalue).getValue().floatValue();
		}
	}

	protected static long toJavaLong(JSONValue jvalue) {
		return ((JSONInteger) jvalue).getValue().longValue();
	}

	protected static int toJavaInteger(JSONValue jvalue) {
		return ((JSONInteger) jvalue).getValue().intValue();
	}

	protected static short toJavaShort(JSONValue jvalue) {
		return ((JSONInteger) jvalue).getValue().shortValue();
	}

	protected static byte toJavaByte(JSONValue jvalue) {
		return ((JSONInteger) jvalue).getValue().byteValue();
	}

	protected static String toJavaString(JSONValue jvalue) {
		return ((JSONString) jvalue).getValue();
	}

	protected static Date toJavaDate(JSONValue jvalue) throws Exception {

		Map data = ((JSONObject) jvalue).getValue();
		JSONInteger ji = (JSONInteger) data.get("time");
		Date date = new Date(ji.getValue().longValue());

		return date;
	}
	
	protected static Map toJavaMap(JSONObject jobj) throws Exception {
		Map map = new HashMap();
		
		HashMap data = jobj.getValue();
		for (Iterator i = data.keySet().iterator(); i.hasNext();) {
			String key = (String) i.next();
			map.put(key, toJavaValue((JSONValue)data.get(key)));
		}
		
		return map;
	}

	protected static Object toJavaObject(JSONObject jobj) throws Exception {
		Object obj = null;

		HashMap data = jobj.getValue();
		String className = ((JSONString) data.get("_javaClass")).getValue();
		data.remove("_javaClass");
		obj = Class.forName(className).newInstance();
		for (Iterator i = data.keySet().iterator(); i.hasNext();) {
			String pname = (String) i.next();
			setProperty(obj, pname, (JSONValue) data.get(pname));
		}

		return obj;
	}

	protected static Class toJavaClass(JSONValue jvalue)
			throws ClassNotFoundException {
		Class clazz = null;

		if (jvalue.isBoolean()) {
			clazz = boolean.class;
		} else if (jvalue.isDecimal()) {
			double value = toJavaDouble(jvalue);
			if (value > Float.MAX_VALUE || value < Float.MIN_VALUE) {
				clazz = double.class;
			} else {
				clazz = float.class;
			}
		} else if (jvalue.isInteger()) {
			long value = toJavaLong(jvalue);
			if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) {
				clazz = long.class;
			} else if (value > Short.MAX_VALUE || value < Short.MIN_VALUE) {
				clazz = int.class;
			} else if (value > Byte.MAX_VALUE || value < Byte.MIN_VALUE) {
				clazz = short.class;
			} else {
				clazz = byte.class;
			}
		} else if (jvalue.isString()) {
			clazz = String.class;
		} else if (jvalue.isObject()) {
			String className = null;
			if(((JSONObject) jvalue).containsKey("_javaClass")) {
				className = ((JSONString) ((JSONObject) jvalue).get("_javaClass")).getValue();
			}			
			if(className!=null && className.trim().length()>0) {
				clazz = Class.forName(className);
			} else {
				clazz = Map.class;
			}			
		} else if (jvalue.isArray()) {
			clazz = List.class;
		} else if (jvalue.isNull()) {
			clazz = Object.class;
		}

		return clazz;
	}

	protected static List toJavaList(JSONArray jarray) throws Exception {

		List list = new ArrayList();

		for (Iterator i = jarray.getValue().iterator(); i.hasNext();) {
			list.add(toJavaValue((JSONValue) i.next()));
		}

		return list;
	}

	protected static void setProperty(Object obj, String name, JSONValue jvalue)
			throws Exception {

		PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(obj, name);

		if (jvalue.isNull()) {
			if (pd.getPropertyType().isPrimitive()) {
				return;
			} else {
				BeanUtils.setProperty(obj, name, null);
			}
		} else if (jvalue.isBoolean()) {
			JSONBoolean bvalue = (JSONBoolean) jvalue;
			BeanUtils.setProperty(obj, name, bvalue.getValue());
		} else if (jvalue.isInteger()) {
			JSONInteger ivalue = (JSONInteger) jvalue;
			if (pd.getPropertyType() == Byte.class
					|| pd.getPropertyType() == byte.class) {
				BeanUtils.setProperty(obj, name, ivalue.getValue().byteValue());
			} else if (pd.getPropertyType() == Short.class
					|| pd.getPropertyType() == short.class) {
				BeanUtils.setProperty(obj, name, ivalue.getValue().shortValue());
			} else if (pd.getPropertyType() == Integer.class
					|| pd.getPropertyType() == int.class) {
				BeanUtils.setProperty(obj, name, ivalue.getValue().intValue());
			} else if (pd.getPropertyType() == Long.class
					|| pd.getPropertyType() == long.class) {
				BeanUtils.setProperty(obj, name, ivalue.getValue().longValue());
			} else if (pd.getPropertyType() == Double.class
					|| pd.getPropertyType() == double.class) {
				BeanUtils.setProperty(obj, name, ivalue.getValue().doubleValue());
			} else if (pd.getPropertyType() == Float.class
					|| pd.getPropertyType() == float.class) {
				BeanUtils.setProperty(obj, name, ivalue.getValue().floatValue());
			}
		} else if (jvalue.isDecimal()) {
			JSONDecimal dvalue = (JSONDecimal) jvalue;
			if (pd.getPropertyType() == Float.class
					|| pd.getPropertyType() == float.class) {
				BeanUtils.setProperty(obj, name, dvalue.getValue().floatValue());
			} else if (pd.getPropertyType() == Double.class
					|| pd.getPropertyType() == double.class) {
				BeanUtils.setProperty(obj, name, dvalue.getValue().doubleValue());
			}
		} else if (jvalue.isString()) {
			JSONString svalue = (JSONString) jvalue;
			BeanUtils.setProperty(obj, name, svalue.getValue());
		} else if (jvalue.isArray()) {
			JSONArray avalue = (JSONArray) jvalue;
			BeanUtils.setProperty(obj, name, toJavaList(avalue));
		} else if (jvalue.isObject()) {
			BeanUtils.setProperty(obj, name, toJavaValue(jvalue));
		}
	}

	protected static String toJSONValue(Object obj) throws Exception {

		if (obj == null) {
			return "null";
		} else if (obj instanceof Boolean) {
			return toJSONBoolean((Boolean) obj);
		} else if (obj instanceof Number) {
			return toJSONNumber(obj);
		} else if (obj instanceof String) {
			return toJSONString((String) obj);
		} else if (obj instanceof List) {
			return toJSONArray((List) obj);
		} else if (obj instanceof Date) {
			return toJSONDate((Date) obj);
		} else if (obj instanceof Map) {
			return toJSONMap((Map) obj);
		} else {
			return toJSONObject(obj);
		}
	}

	protected static String toJSONBoolean(boolean value) {
		return value ? "true" : "false";
	}

	protected static String toJSONNumber(Object value) {
		return "" + value;
	}

	protected static String toJSONString(String value) {
		return quote(value);
	}

	protected static String toJSONArray(List list) throws Exception {

		String res = "";
		for (Iterator i = list.iterator(); i.hasNext();) {
			res += (res.length() > 0 ? "," : "");
			res += toJSONValue(i.next());
		}
		return "[" + res + "]";

	}

	protected static String toJSONDate(Date date) throws Exception {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
		return "{\"string\":\"" + sdf.format(date) + "\",\"time\":"
				+ date.getTime() + ",\"_javaClass\":\"java.util.Date\"}";
	}
	
	protected static String toJSONMap(Map map) throws Exception {
		String res = "";

		for(Iterator i=map.keySet().iterator();i.hasNext();) {
			String name  = (String)i.next();
			Object value = map.get(name);
			res += (res.length() > 0 ? "," : "");
			res += "\"" + name + "\":" + toJSONValue(value);
		}
		res += (res.length() > 0 ? "," : "");
		res += "\"_javaClass\":\"java.util.Map\"";

		return "{" + res + "}";
	}

	protected static String toJSONObject(Object obj) throws Exception {
		String res = "";

		PropertyDescriptor[] pds = PropertyUtils.getPropertyDescriptors(obj
				.getClass());
		String className = obj.getClass().getName();
		String name = null;
		Object value = null;
		for (int i = 0; i < pds.length; i++) {
			PropertyDescriptor pd = pds[i];
			name = pd.getName();
			value = PropertyUtils.getProperty(obj, name);
			if (!name.equals("class")) {
				res += (res.length() > 0 ? "," : "");
				res += "\"" + name + "\":" + toJSONValue(value);
			}
		}
		res += (res.length() > 0 ? "," : "");
		res += "\"_javaClass\":\"" + className + "\"";

		return "{" + res + "}";
	}

	/**
	 * Produce a string in double quotes with backslash sequences in all the
	 * right places. A backslash will be inserted within </, allowing JSON text
	 * to be delivered in HTML. In JSON text, a string cannot contain a control
	 * character or an unescaped quote or backslash.
	 * 
	 * @param string
	 *            A String
	 * @return A String correctly formatted for insertion in a JSON text.
	 */
	protected static String quote(String string) {
		if (string == null || string.length() == 0) {
			return "\"\"";
		}

		char b;
		char c = 0;
		int i;
		int len = string.length();
		StringBuffer sb = new StringBuffer(len + 4);
		String t;

		sb.append('"');
		for (i = 0; i < len; i += 1) {
			b = c;
			c = string.charAt(i);
			switch (c) {
			case '\\':
			case '"':
				sb.append('\\');
				sb.append(c);
				break;
			case '/':
				if (b == '<') {
					sb.append('\\');
				}
				sb.append(c);
				break;
			case '\b':
				sb.append("\\b");
				break;
			case '\t':
				sb.append("\\t");
				break;
			case '\n':
				sb.append("\\n");
				break;
			case '\f':
				sb.append("\\f");
				break;
			case '\r':
				sb.append("\\r");
				break;
			default:
				if (c < ' ') {
					t = "000" + Integer.toHexString(c);
					sb.append("\\u" + t.substring(t.length() - 4));
				} else {
					sb.append(c);
				}
			}
		}
		sb.append('"');
		return sb.toString();
	}

}
